package nr.co.mhgames.polyanim.renderer;

import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import nr.co.mhgames.polyanim.Animation;
import nr.co.mhgames.polyanim.Group;
import nr.co.mhgames.polyanim.KeyFrame;
import nr.co.mhgames.polyanim.Polygon;
import nr.co.mhgames.polyanim.renderer.modifiers.AlphaModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.ColorModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.FlipModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.HSLModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.RenderModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.RotateModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.ScaleModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.FlipModifier.FlipDirection;
import nr.co.mhgames.polyanim.util.BasicTriangulator;
import nr.co.mhgames.polyanim.util.ColorDef;
import nr.co.mhgames.polyanim.util.Matrix;
import nr.co.mhgames.polyanim.util.Triangulator;
import nr.co.mhgames.polyanim.util.Vector2f;
import nr.co.mhgames.polyanim.util.Vertex;

import org.lwjgl.opengl.GL11;

/**
 * LWJGL renderer for PolyAnim. It uses OpenGL API through LWJGL to render the
 * animations, and is thus quite fast.
 * <p>
 * The renderer doesn't support self-intersecting polygons, unlike the Java 2D
 * renderer. It is recommended that the polygons are convex and simple, since
 * OpenGL doesn't guarantee that non-convex polygons will be rendered properly
 * (most likely they won't). For user convenience, the renderer now supports
 * automatic triangulation for concave polygons (
 * {@link LWJGLAnimRenderer#enableAutoTriangulation(boolean)}). This results in
 * speed penalty, so convex polygons should be preferred.
 * 
 * @author Mika Halttunen
 */
public class LWJGLAnimRenderer extends AnimRenderer {
	/** Singleton instance */
	private static LWJGLAnimRenderer singleton;

	/** Does the renderer perform triangulation on the polygons? */
	private boolean doTriangulation;

	/** Triangulator instance */
	private Triangulator triangulator;


	/**
	 * Private constructor, you must use {@link #getInstance()} instead.
	 */
	private LWJGLAnimRenderer() {
		// Set up alpha blending
		GL11.glEnable(GL11.GL_BLEND);
		GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

		doTriangulation = false;
	}

	/**
	 * Return the LWJGL renderer instance.
	 * 
	 * @return LWJGL renderer
	 */
	public static AnimRenderer getInstance() {
		if (singleton == null) {
			singleton = new LWJGLAnimRenderer();
		}

		return singleton;
	}

	public String getIdentifier() {
		return "LWJGL";
	}

	/**
	 * Enable or disable automatic polygon triangulation for concave polygons.
	 * By enabling triangulation, the renderer automatically turns any concave
	 * polygons into several triangles and renders them instead. Assuming the
	 * animation has concave polygons, the renderer output will be correct only
	 * when the triangulation is enabled.
	 * <p>
	 * Note: automatic triangulation should be used with caution as it will slow
	 * down the renderer. Using convex polygons is still recommended when the
	 * most speed is desired.
	 * 
	 * @param triangulate
	 *            If true, the renderer will automatically triangulate any
	 *            concave polygons.
	 */
	public void enableAutoTriangulation(boolean triangulate) {
		this.doTriangulation = triangulate;
		if (triangulate && triangulator == null)
			triangulator = new BasicTriangulator();
	}

	/**
	 * Returns whether the automatic polygon triangulation is enabled or not.
	 * 
	 * @see {@link LWJGLAnimRenderer#enableAutoTriangulation(boolean)}
	 * @return true if the triangulation is enabled
	 */
	public boolean isEnabledAutoTriangulation() {
		return this.doTriangulation;
	}

	public void renderPolygon(Polygon poly) {
		// Set the color (multiply with the alpha factor)
		ColorDef color = poly.getColor();

		// Use the precreated ColorDef instance, to avoid garbage
		tmpColor.set(color.red, color.green, color.blue,
				(int) (color.alpha * ActiveModifiers.alphaFactor));
		// Modulate the color
		if (ActiveModifiers.activeHSL != null) {
			ActiveModifiers.activeHSL.modulate(tmpColor);
		}

		// Coloring?
		if (ActiveModifiers.activeColoring != null)
			ActiveModifiers.activeColoring.transformColor(tmpColor);

		// Apply the color to GL
		GL11.glColor4ub((byte) tmpColor.red, (byte) tmpColor.green, (byte) tmpColor.blue,
				(byte) tmpColor.alpha);


		// Render convex polygons as is
		if (!doTriangulation || poly.isConvex()) {
			GL11.glBegin(GL11.GL_POLYGON);
			for (Vertex v: poly.getVertices()) {
				GL11.glVertex2f(v.position.x, v.position.y);
			}
			GL11.glEnd();
		} else {
			// Triangulate first, then render
			List<Polygon> polys = triangulator.triangulatePolygon(poly);
			for (Iterator<Polygon> pit = polys.iterator(); pit.hasNext();) {
				Polygon p = pit.next();
				GL11.glBegin(GL11.GL_TRIANGLES);
				for (Iterator<Vertex> it = p.getVertices().iterator(); it.hasNext();) {
					Vertex v = it.next();
					GL11.glVertex2f(v.position.x, v.position.y);
				}
				GL11.glEnd();
			}
		}

		// Render lines
		if (shouldDrawOutlines()) {
			GL11.glColor3f(0, 0, 0);
			GL11.glBegin(GL11.GL_LINE_LOOP);
			for (Vertex v: poly.getVertices()) {
				GL11.glVertex2f(v.position.x, v.position.y);
			}
			GL11.glEnd();
		}
	}

	public void renderGroup(Group group) {
		GL11.glPushMatrix();

		// Translate to the offset
		Vector2f offset = group.getOffset();
		GL11.glTranslatef(offset.x, offset.y, 0);

		// Rotate around the pivot point
		if (group.getRotation() != 0.0f) {
			Vector2f pivot = group.getPivot();
			GL11.glTranslatef(pivot.x, pivot.y, 0);
			GL11.glRotatef(group.getRotation(), 0, 0, 1);
			GL11.glTranslatef(-pivot.x, -pivot.y, 0);
		}

		// Scale
		Vector2f scale = group.getScale();
		GL11.glScalef(scale.x, scale.y, 1.0f);

		// Draw the group polygons
		for (Polygon poly: group.getPolygons())
			renderPolygon(poly);

		GL11.glPopMatrix();


		// TODO: Remove this test
		// Matrix tr = group.computeTransforms();
		// Vector2f point = tr
		// .transformPoint(group.getPolygon(0).getVertex(0).position);
		// Vector2f point2 = tr
		// .transformPoint(group.getPolygon(0).getVertex(1).position);
		// GL11.glColor3f(1, 1, 1);
		// GL11.glBegin(GL11.GL_LINES);
		// GL11.glVertex2f(point.x, point.y);
		// GL11.glVertex2f(point2.x, point2.y);
		// GL11.glEnd();
		//
		// GL11.glPushMatrix();
		// applyMatrix(tr);
		//
		// Polygon poly = group.getPolygon(0);
		// GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE);
		// Color c = poly.getColor();
		// poly.setColor(Color.white);
		// renderPolygon(poly);
		// poly.setColor(c);
		// GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
		// GL11.glPopMatrix();
	}

	public void renderKeyFrame(KeyFrame frame) {
		// We need to make a copy of the group map values as not to disturb the
		// original order.
		HashMap<String, Group> groupMap = frame.getGroups();
		ArrayList<Group> groups = new ArrayList<Group>(groupMap.values());

		// Sort by the depth value
		Collections.sort(groups);

		// Draw the polygon groups
		for (Group g: groups) {
			if (g.isVisible())
				renderGroup(g);
		}
	}

	public void renderAnimation(Animation anim) {
		renderKeyFrame(anim.getCurrentFrame());
	}

	public void renderAnimation(Animation anim, float offsetX, float offsetY) {
		GL11.glPushMatrix();

		// Translate to the offset and render
		GL11.glTranslatef(offsetX, offsetY, 0);
		renderAnimation(anim);

		GL11.glPopMatrix();
	}

	@Override
	public void renderAnimation(Animation anim, float offsetX, float offsetY,
			RenderModifier... modifiers) {
		GL11.glPushMatrix();

		// Translate to the offset
		GL11.glTranslatef(offsetX, offsetY, 0);

		// Store the HSL and color modifiers
		storeRenderModifiers();

		// Apply the modifiers
		for (RenderModifier mod: modifiers)
			applyRenderModifier(mod);

		// Render
		renderAnimation(anim);
		GL11.glPopMatrix();

		// Restore the HSL and color modifier
		restoreRenderModifiers();
	}

	/**
	 * Applies the given render modifier for the next rendering call.
	 * 
	 * @param mod
	 *            Render modifier
	 */
	private void applyRenderModifier(RenderModifier mod) {
		// Flips
		if (FlipModifier.class.isInstance(mod)) {
			FlipModifier m = (FlipModifier) mod;
			if (m.direction == FlipDirection.VERTICAL)
				GL11.glScalef(1, -1, 1);
			else if (m.direction == FlipDirection.HORIZONTAL)
				GL11.glScalef(-1, 1, 1);
			else
				GL11.glScalef(-1, -1, 1);
		}
		// Rotations
		else if (RotateModifier.class.isInstance(mod)) {
			RotateModifier m = (RotateModifier) mod;
			GL11.glRotatef(m.rotation, 0, 0, 1);
		}
		// Scale
		else if (ScaleModifier.class.isInstance(mod)) {
			ScaleModifier m = (ScaleModifier) mod;
			GL11.glScalef(m.scaleX, m.scaleY, 1);
		}
		// HSB
		else if (HSLModifier.class.isInstance(mod)) {
			ActiveModifiers.activeHSL = (HSLModifier) mod;
		}
		// Coloring
		else if (ColorModifier.class.isInstance(mod)) {
			ActiveModifiers.activeColoring = (ColorModifier) mod;
		}
		// Alpha modifier
		else if (AlphaModifier.class.isInstance(mod)) {
			ActiveModifiers.alphaFactor = ((AlphaModifier) mod).factor;
		}
	}

	/**
	 * Multiply the OpenGL matrix by the given matrix.
	 * 
	 * @param mat
	 *            Matrix to multiply with
	 */
	public void applyMatrix(Matrix mat) {
		float m[] = new float[16];
		mat.getM(m);

		FloatBuffer buf = FloatBuffer.wrap(m);
		GL11.glMultMatrix(buf);
	}
}
